package compi;

import java.io.*;
import java.nio.CharBuffer;
import java.util.NoSuchElementException;
import java.util.Scanner;

import data.Afd;
import data.TipoToken;
import exceptions.EOF;

public class Analex 
{
	private int _numLinea;
	private Scanner _scan;
	
	private CharBuffer _lineBuffer;
	private CharBuffer _prevLineBuffer;
	private int _inicioUltimoToken;
	
	private CharBuffer _tokenBuffer;
	private Afd _automata;
	private String _lookahead;
	private int _estadoAutomata;
	private int _lineLength;
	private int _inicioComentario;

	private final int MAX_LINE_LENGTH = 120;
	private final int MAX_TOKEN_LENGTH = 1024;

	/**
	 * Palabras reservadas
	 */
	private String palabrasReservadas[] =
	{
			"programa","comienzo","fin","variables","entero","real","procedimiento",
			"entrada","salida","si","entonces","hacer","mientras","salir","get",
			"put_line"
	};

	/**
	 * Constructor
	 * @param fName
	 * @throws FileNotFoundException
	 */
	public Analex(String fName) throws FileNotFoundException
	{
		_numLinea = _estadoAutomata = 0;
		_inicioComentario = -1;
		
		_scan = new Scanner(new File(fName));
		
		_lineBuffer = CharBuffer.allocate(MAX_LINE_LENGTH);
		_prevLineBuffer = CharBuffer.allocate(MAX_LINE_LENGTH);
		_inicioUltimoToken = -1;
		
		_tokenBuffer = CharBuffer.allocate(MAX_TOKEN_LENGTH);
		
		_automata = new Afd();
	}

	private char tipoCaracter(char c) 
	{

		/*
		 * dado un caracter lo clasifica como digito, o letra en cualquier otro
		 * caso devuelve el caracter
		 */
		
		if (c >= '0' && c <= '9')
			return 'd';

		if (c=='e' || c=='E')
			return 'e';
		
		if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
			return 'l';

		return c;

	}

	private int traducirSimbolo(char car) {

		/*
		 * dado un tipo de caracter devolvemos el codigo del simbolo que
		 * reconoce el AFD.
		 */

		char tdc = tipoCaracter(car);
		switch (tdc) {

		case 'l':
			return 0;

		case 'd':
			return 1;
		
		case 'e':
			return 2;

		case '_':
			return 3;

		case '.':
			return 4;

		case ':':
		case '(':
		case ')':
		case ',':
		case ';':
			return 5;
			
		case '+':
			return 6;
		case '-':
			return 7;
		
		case '>':
		case '<':
			return 8;
		
		case '=':
			return 9;

		case '/':
			return 10;
			
		case '*':
			return 11;
			
		default:
			return 12;
		}

	}

	/**
	 * esBlanco
	 * @param a
	 * @return a == blanco
	 */
	private boolean esBlanco(char a) 
	{
		return (a == ' ' || a == '\t');
	}
	
	/**
	 * esSeparador
	 * @param a
	 * @return a == separador
	 */
	private boolean esSeparador(char a)
	{
		return (a == ',' || a == ';' || a == ':');
	}
	
	/**
	 * esSeparadorDeToken
	 * @param c
	 * @return c == caracter valido para fin de token
	 */
	public boolean esSeparadorDeToken(char c)
	{
		return esBlanco(c) || esSeparador(c) || esOperador(c);
	}
	
	/**
	 * esOperador
	 * @param c
	 * @return c == operador valido
	 */
	public boolean esOperador(char c)
	{
		return c == '*' || c == '+' || c == '-' || c == '/' || c == '<' 
			|| c == '>' || c == '=' || c == '(' || c == ')';
	}

	/**
	 * saltarBlancos
	 */
	public void saltarBlancos()
	{
		while ( esBlanco(_lineBuffer.get()) ); // End while
		
		_lineBuffer.position(_lineBuffer.position()-1);
	}
	
	/**
	 * saltarLineasVacias
	 * @throws EOF
	 * @throws IOException
	 */
	public void saltarLineasVacias() throws EOF, IOException
	{
		// Salta lineas vacias
		while ( finLinea() )
		{
			nuevaLinea();
			saltarBlancos();
		}
	}

	/**
	 * esReservada
	 * @param tipo
	 * @return tipo = palabra reservada
	 */
	private boolean esReservada(String tipo) 
	{
		for (int i = 0; i < palabrasReservadas.length; i++) 
		{			
			if ( palabrasReservadas[i].equals(tipo) ) 
				return true;
		}

		return false;
	}

	/**
	 * nextToken
	 * @throws EOF
	 * @throws IOException
	 */
	public void nextToken() throws EOF, IOException
	{
		boolean tokenEncontrado = false;
		int estadoActual = 0;
		boolean comentarioIniciado = false;
		char cAnterior = '_';
		_estadoAutomata = -1;
		
		// Lee nueva linea si el buffer esta vacio
		if ( finLinea() )
			nuevaLinea();
		
		// Saltar espacios en blanco
		saltarBlancos();
		
		// Saltar lineas vacias
		saltarLineasVacias();
		
		_inicioUltimoToken = _lineBuffer.position();
		
		/**
		 * Hasta que encuentre un token
		 */
		while ( !tokenEncontrado )
		{
			if ( finLinea() )
				if ( estadoComentario(_estadoAutomata) )
					saltarLineasVacias();// Lee nueva linea y salta las vacias
				else // Fin de linea, y por lo tanto, de token
					break;
			
			// Extraer caracter y añadirlo al buffer de linea
			char c = _lineBuffer.get();

			// Si leemos un blanco -> fin de token
			if ( esBlanco(c) && !estadoComentario(_estadoAutomata) )
				break;

			// Obtenemos el siguiente estado de la transicion
			estadoActual = _automata.transicion(estadoActual, traducirSimbolo(c));

			// Guardamos el inicio del comentario
			if ( !comentarioIniciado && estadoComentario(estadoActual) )
			{
				_inicioComentario = numLinea();
				comentarioIniciado = true;
			}
			
			// Si estadoActual < 0 el token termina (con o sin error lexico)
			if ( estadoActual < 0 )
			{
				tokenEncontrado = true;
				
				if ( !_automata.esFinal(_estadoAutomata)
						|| (!esSeparadorDeToken(c) && !esSeparadorDeToken(cAnterior)) )
				{ // Error lexico
					if ( !esSeparadorDeToken(c) )
						// Acotar error hasta encontrar blanco o separador
						acotarErrorLexico(c);

					_estadoAutomata = -1;
				}
				
				// Rebobinar buffer
				_lineBuffer.position(_lineBuffer.position()-1);
				
			} 
			else 
			{	// El estado del automata es un estado valido
				_tokenBuffer.put(c);
				_estadoAutomata = estadoActual;
			}

			// Guardar el ultimo caracter
			cAnterior = c;
		}
		
		/**
		 * Guardar el token leido
		 */
		int tokenEnd = _tokenBuffer.position();
		_tokenBuffer.rewind();
		_lookahead = new String(_tokenBuffer.subSequence(0, tokenEnd).toString());
		_tokenBuffer.clear();
	}
	
	/**
	 * acotarErrorLexico
	 * @param c
	 */
	private void acotarErrorLexico(char c)
	{
		_tokenBuffer.put(c);
		boolean seguir = true;
		
		while (seguir)
		{
			if ( _lineBuffer.position() == _lineLength )
				// El error lexico termina al final de la linea
				break;

			char aux = _lineBuffer.get();
			
			if ( esSeparadorDeToken(aux) )
				// El error lexico termina en el siguiente separador de token
				break;
			else
				_tokenBuffer.put(aux);
		}
	}


	/**
	 * nuevaLinea
	 * @throws EOF
	 * @throws IOException
	 */
	private void nuevaLinea() throws EOF, IOException
	{
		if ( _lineBuffer.length() > 0 )
		{
			_prevLineBuffer.clear();
			_prevLineBuffer.put(_lineBuffer);
			_lineBuffer.clear();
		}

		try
		{
			String aux = _scan.nextLine().trim();
			_lineBuffer.put(aux);
			_lineBuffer.rewind();
			_lineLength = aux.length();
			_numLinea++;
		} catch (NoSuchElementException e)
		{
			throw new EOF();
		} 
		catch (Exception e)
		{
			throw new IOException();
		}
	}
	
	/**
	 * finLinea
	 * @return el puntero del buffer se encuentra al final de la linea
	 */
	public boolean finLinea()
	{
		return _lineBuffer.position() == _lineLength;
	}

	/**
	 * nombreToken
	 * @return contenido del token
	 */
	public String nombreToken()
	{
		return _lookahead;
	}

	/**
	 * tipoToken
	 * @return Tipo de token dependiendo del último estado en el que se encontraba
	 * 			el automata
	 */
	public TipoToken tipoToken()
	{
		switch ( _estadoAutomata ) 
		{
		case 1: // Identificadores
		case 3:
			if (esReservada(_lookahead)) 
			{
				return TipoToken.PAL_RESERV;
			} else {
				return TipoToken.IDENT;
			}

		case 2: // Enteros
			return TipoToken.NUM_ENTERO;
			
		case 5: // Numeros reales
		case 8:
			return TipoToken.NUM_REAL;
			
		case 9: // Operadores
		case 10:
		case 11:
		case 12:
		case 13:
		case 14:
		case 15:
			return TipoToken.OPERADOR;
			
		case 19: // Comentarios
			return TipoToken.COMMENTS;

		default: // Error
			return TipoToken.ERROR;
		}

	}// end Tipo_Token

	/**
	 * numLinea
	 * @return numero de la ultima linea leida
	 */
	public int numLinea() {

		return _numLinea;

	}
	
	/**
	 * estadoComentario
	 * @param estado
	 * @return estado es un estado de comentario
	 */
	public boolean estadoComentario(int estado)
	{
		return (estado >= _automata.INICIO_COMENTARIO
				&& estado <= _automata.FIN_COMENTARIO);
	}
	
	/**
	 * inicioComentario
	 * @return linea en la que se inicio el ultimo comentario
	 */
	public int inicioComentario()
	{
		if ( !estadoComentario(_estadoAutomata) )
			return -1;
		else
			return _inicioComentario;
	}
	
	/**
	 * estado
	 * @return estado actual del automata
	 */
	public int estado()
	{
		return _estadoAutomata;
	}
	
	/**
	 * Retroceder
	 */
	public void retroceder()
	{
		_lineBuffer.position(_inicioUltimoToken);
	}

}

